/*******************************************************************************
 * Copyright (c) 2009-2011 Luaj.org. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a copy
 * of this software and associated documentation files (the "Software"), to deal
 * in the Software without restriction, including without limitation the rights
 * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
 * copies of the Software, and to permit persons to whom the Software is
 * furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
 * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
 * THE SOFTWARE.
 ******************************************************************************/
package com.org.luaj.lib.jse;

import com.org.luaj.LuaTable;
import com.org.luaj.LuaString;
import com.org.luaj.LuaValue;

import java.lang.reflect.Array;
import java.nio.charset.StandardCharsets;
import java.util.Collections;
import java.util.HashMap;
import java.util.Map;

/**
 * Helper class to coerce values from lua to Java within the luajava library.
 * <p>
 * This class is primarily used by the {@link LuajavaLib},
 * but can also be used directly when working with Java/lua bindings.
 * <p>
 * To coerce to specific Java values, generally the {@code toType()} methods
 * on {@link LuaValue} may be used:
 * <ul>
 * <li>{@link LuaValue#toboolean()}</li>
 * <li>{@link LuaValue#tobyte()}</li>
 * <li>{@link LuaValue#tochar()}</li>
 * <li>{@link LuaValue#toshort()}</li>
 * <li>{@link LuaValue#toint()}</li>
 * <li>{@link LuaValue#tofloat()}</li>
 * <li>{@link LuaValue#todouble()}</li>
 * <li>{@link LuaValue#tojstring()}</li>
 * <li>{@link LuaValue#touserdata()}</li>
 * <li>{@link LuaValue#touserdata(Class)}</li>
 * </ul>
 * <p>
 * For data in lua tables, the various methods on {@link LuaTable} can be used directly
 * to convert data to something more useful.
 *
 * @see LuajavaLib
 * @see CoerceJavaToLua
 */
public class CoerceLuaToJava {

    static int SCORE_NULL_VALUE = 0x10;
    static int SCORE_WRONG_TYPE = 0x100;
    static int SCORE_UNCOERCIBLE = 0x10000;

    static interface Coercion {
        public int score(LuaValue value);

        public Object coerce(LuaValue value);
    }

    ;

    /**
     * Coerce a LuaValue value to a specified java class
     *
     * @param value LuaValue to coerce
     * @param clazz Class to coerce into
     * @return Object of type clazz (or a subclass) with the corresponding value.
     */
    public static Object coerce(LuaValue value, Class clazz) {
        return getCoercion(clazz).coerce(value);
    }

    static final Map COERCIONS = Collections.synchronizedMap(new HashMap());

    static final class BoolCoercion implements Coercion {
        public String toString() {
            return "BoolCoercion()";
        }

        public int score(LuaValue value) {
            switch (value.type()) {
                case LuaValue.TBOOLEAN:
                    return 0;
            }
            return 1;
        }

        public Object coerce(LuaValue value) {
            return value.toboolean() ? Boolean.TRUE : Boolean.FALSE;
        }
    }

    static final class NumericCoercion implements Coercion {
        static final int TARGET_TYPE_BYTE = 0;
        static final int TARGET_TYPE_CHAR = 1;
        static final int TARGET_TYPE_SHORT = 2;
        static final int TARGET_TYPE_INT = 3;
        static final int TARGET_TYPE_LONG = 4;
        static final int TARGET_TYPE_FLOAT = 5;
        static final int TARGET_TYPE_DOUBLE = 6;
        static final String[] TYPE_NAMES = {"byte", "char", "short", "int", "long", "float", "double"};
        final int targetType;

        public String toString() {
            return "NumericCoercion(" + TYPE_NAMES[targetType] + ")";
        }

        NumericCoercion(int targetType) {
            this.targetType = targetType;
        }

        public int score(LuaValue value) {
            if (value.isint()) {
                switch (targetType) {
                    case TARGET_TYPE_BYTE: {
                        int i = value.toint();
                        return (i == (byte) i) ? 0 : SCORE_WRONG_TYPE;
                    }
                    case TARGET_TYPE_CHAR: {
                        int i = value.toint();
                        return (i == (byte) i) ? 1 : (i == (char) i) ? 0 : SCORE_WRONG_TYPE;
                    }
                    case TARGET_TYPE_SHORT: {
                        int i = value.toint();
                        return (i == (byte) i) ? 1 : (i == (short) i) ? 0 : SCORE_WRONG_TYPE;
                    }
                    case TARGET_TYPE_INT: {
                        int i = value.toint();
                        return (i == (byte) i) ? 2 : ((i == (char) i) || (i == (short) i)) ? 1 : 0;
                    }
                    case TARGET_TYPE_FLOAT:
                        return 1;
                    case TARGET_TYPE_LONG:
                        return 1;
                    case TARGET_TYPE_DOUBLE:
                        return 2;
                    default:
                        return SCORE_WRONG_TYPE;
                }
            } else if (value.isnumber()) {
                switch (targetType) {
                    case TARGET_TYPE_BYTE:
                        return SCORE_WRONG_TYPE;
                    case TARGET_TYPE_CHAR:
                        return SCORE_WRONG_TYPE;
                    case TARGET_TYPE_SHORT:
                        return SCORE_WRONG_TYPE;
                    case TARGET_TYPE_INT:
                        return SCORE_WRONG_TYPE;
                    case TARGET_TYPE_LONG: {
                        double d = value.todouble();
                        return (d == (long) d) ? 0 : SCORE_WRONG_TYPE;
                    }
                    case TARGET_TYPE_FLOAT: {
                        double d = value.todouble();
                        return (d == (float) d) ? 0 : SCORE_WRONG_TYPE;
                    }
                    case TARGET_TYPE_DOUBLE: {
                        double d = value.todouble();
                        return ((d == (long) d) || (d == (float) d)) ? 1 : 0;
                    }
                    default:
                        return SCORE_WRONG_TYPE;
                }
            } else {
                return SCORE_UNCOERCIBLE;
            }
        }

        public Object coerce(LuaValue value) {
            switch (targetType) {
                case TARGET_TYPE_BYTE:
                    return new Byte((byte) value.toint());
                case TARGET_TYPE_CHAR:
                    return new Character((char) value.toint());
                case TARGET_TYPE_SHORT:
                    return new Short((short) value.toint());
                case TARGET_TYPE_INT:
                    return new Integer((int) value.toint());
                case TARGET_TYPE_LONG:
                    return new Long((long) value.todouble());
                case TARGET_TYPE_FLOAT:
                    return new Float((float) value.todouble());
                case TARGET_TYPE_DOUBLE:
                    return new Double((double) value.todouble());
                default:
                    return null;
            }
        }
    }

    static final class StringCoercion implements Coercion {
        public static final int TARGET_TYPE_STRING = 0;
        public static final int TARGET_TYPE_BYTES = 1;
        final int targetType;

        public StringCoercion(int targetType) {
            this.targetType = targetType;
        }

        public String toString() {
            return "StringCoercion(" + (targetType == TARGET_TYPE_STRING ? "String" : "byte[]") + ")";
        }

        public int score(LuaValue value) {
            switch (value.type()) {
                case LuaValue.TSTRING:
                    return value.checkstring().isValidUtf8() ?
                            (targetType == TARGET_TYPE_STRING ? 0 : 1) :
                            (targetType == TARGET_TYPE_BYTES ? 0 : SCORE_WRONG_TYPE);
                case LuaValue.TNIL:
                    return SCORE_NULL_VALUE;
                default:
                    return targetType == TARGET_TYPE_STRING ? SCORE_WRONG_TYPE : SCORE_UNCOERCIBLE;
            }
        }

        public Object coerce(LuaValue value) {
            if (value.isnil())
                return null;
            if (targetType == TARGET_TYPE_STRING)
                return value.tojstring();
            LuaString s = value.checkstring();
            byte[] b = new byte[s.m_length];
            s.copyInto(0, b, 0, b.length);
            return b;
        }
    }

    static final class ArrayCoercion implements Coercion {
        final Class componentType;
        final Coercion componentCoercion;

        public ArrayCoercion(Class componentType) {
            this.componentType = componentType;
            this.componentCoercion = getCoercion(componentType);
        }

        public String toString() {
            return "ArrayCoercion(" + componentType.getName() + ")";
        }

        public int score(LuaValue value) {
            switch (value.type()) {
                case LuaValue.TTABLE:
                    return value.length() == 0 ? 0 : componentCoercion.score(value.get(1));
                case LuaValue.TUSERDATA:
                    return inheritanceLevels(componentType, value.touserdata().getClass().getComponentType());
                case LuaValue.TNIL:
                    return SCORE_NULL_VALUE;
                default:
                    return SCORE_UNCOERCIBLE;
            }
        }

        public Object coerce(LuaValue value) {
            switch (value.type()) {
                case LuaValue.TTABLE: {
                    int n = value.length();
                    Object a = Array.newInstance(componentType, n);
                    for (int i = 0; i < n; i++)
                        Array.set(a, i, componentCoercion.coerce(value.get(i + 1)));
                    return a;
                }
                case LuaValue.TSTRING:// 20220503新增，只用于将lua内部的字符串转换为原始byte数组
                    if (this.componentType == byte[].class) {
                        LuaString s = value.checkstring();
                        byte[] b = new byte[s.m_length];
                        s.copyInto(0, b, 0, b.length);
                        return b;
                    } else {
                        return value.tojstring();
                    }
                case LuaValue.TUSERDATA:
                    return value.touserdata();
                case LuaValue.TNIL:
                    return null;
                default:
                    String str = value.tojstring();
                    if (this.componentType == byte[].class) {
                        return str.getBytes(StandardCharsets.UTF_8);
                    } else {
                        return str;
                    }
            }
        }
    }

    /**
     * Determine levels of inheritance between a base class and a subclass
     *
     * @param baseclass base class to look for
     * @param subclass  class from which to start looking
     * @return number of inheritance levels between subclass and baseclass,
     * or SCORE_UNCOERCIBLE if not a subclass
     */
    static final int inheritanceLevels(Class baseclass, Class subclass) {
        if (subclass == null)
            return SCORE_UNCOERCIBLE;
        if (baseclass == subclass)
            return 0;
        int min = Math.min(SCORE_UNCOERCIBLE, inheritanceLevels(baseclass, subclass.getSuperclass()) + 1);
        Class[] ifaces = subclass.getInterfaces();
        for (int i = 0; i < ifaces.length; i++)
            min = Math.min(min, inheritanceLevels(baseclass, ifaces[i]) + 1);
        return min;
    }

    static final class ObjectCoercion implements Coercion {
        final Class targetType;

        ObjectCoercion(Class targetType) {
            this.targetType = targetType;
        }

        public String toString() {
            return "ObjectCoercion(" + targetType.getName() + ")";
        }

        public int score(LuaValue value) {
            switch (value.type()) {
                case LuaValue.TNUMBER:
                    return inheritanceLevels(targetType, value.isint() ? Integer.class : Double.class);
                case LuaValue.TBOOLEAN:
                    return inheritanceLevels(targetType, Boolean.class);
                case LuaValue.TSTRING:
                    return inheritanceLevels(targetType, String.class);
                case LuaValue.TUSERDATA:
                    return inheritanceLevels(targetType, value.touserdata().getClass());
                case LuaValue.TNIL:
                    return SCORE_NULL_VALUE;
                default:
                    return inheritanceLevels(targetType, value.getClass());
            }
        }

        public Object coerce(LuaValue value) {
            switch (value.type()) {
                case LuaValue.TNUMBER:
                    return value.isint() ? (Object) new Integer(value.toint()) : (Object) new Double(value.todouble());
                case LuaValue.TBOOLEAN:
                    return value.toboolean() ? Boolean.TRUE : Boolean.FALSE;
                case LuaValue.TSTRING:
                    return value.tojstring();
                case LuaValue.TUSERDATA:
                    return value.optuserdata(targetType, null);
                case LuaValue.TNIL:
                    return null;
                default:
                    return value;
            }
        }
    }

    static {
        Coercion boolCoercion = new BoolCoercion();
        Coercion byteCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_BYTE);
        Coercion charCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_CHAR);
        Coercion shortCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_SHORT);
        Coercion intCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_INT);
        Coercion longCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_LONG);
        Coercion floatCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_FLOAT);
        Coercion doubleCoercion = new NumericCoercion(NumericCoercion.TARGET_TYPE_DOUBLE);
        Coercion stringCoercion = new StringCoercion(StringCoercion.TARGET_TYPE_STRING);
        Coercion bytesCoercion = new StringCoercion(StringCoercion.TARGET_TYPE_BYTES);

        COERCIONS.put(Boolean.TYPE, boolCoercion);
        COERCIONS.put(Boolean.class, boolCoercion);
        COERCIONS.put(Byte.TYPE, byteCoercion);
        COERCIONS.put(Byte.class, byteCoercion);
        COERCIONS.put(Character.TYPE, charCoercion);
        COERCIONS.put(Character.class, charCoercion);
        COERCIONS.put(Short.TYPE, shortCoercion);
        COERCIONS.put(Short.class, shortCoercion);
        COERCIONS.put(Integer.TYPE, intCoercion);
        COERCIONS.put(Integer.class, intCoercion);
        COERCIONS.put(Long.TYPE, longCoercion);
        COERCIONS.put(Long.class, longCoercion);
        COERCIONS.put(Float.TYPE, floatCoercion);
        COERCIONS.put(Float.class, floatCoercion);
        COERCIONS.put(Double.TYPE, doubleCoercion);
        COERCIONS.put(Double.class, doubleCoercion);
        COERCIONS.put(String.class, stringCoercion);
        COERCIONS.put(byte[].class, bytesCoercion);
    }

    static Coercion getCoercion(Class c) {
        Coercion co = (Coercion) COERCIONS.get(c);
        if (co != null) {
            return co;
        }
        if (c.isArray()) {
            Class typ = c.getComponentType();
            co = new ArrayCoercion(c.getComponentType());
        } else {
            co = new ObjectCoercion(c);
        }
        COERCIONS.put(c, co);
        return co;
    }
}
